# 👉 理解 JavaScript 的 async/await

通过上一篇 浏览器的 JavaScript 事件循环机制 (opens new window) 笔记理清事件循环机制以后,接下来进一步看看涉及更多种异步情况的例子,再理一理再品一品~

Promise 是替代传统回调函数的一个常用方案,但当多个异步回调时会导致代码中大量出现 then 方法,为了让代码看起来更加扁平化,因此此处了新的一套解决方案 async/await,async/await 实现也离不开 Promise。

我们先来看看关于 Promise 的执行顺序:

# Promise 链式调用的执行顺序

# Promise 的基础知识

关于 promise 的一些事 (opens new window)

如果对 Promise 还有些不太熟悉,这里可移步至另外一篇文章,梳理了一些关于 Promise 的一些常用知识点,了解完以后,再回来继续往下看。(大神们就此跳过吧~)

# Promise.prototype.then

先来看一个问题:

const promise1 = new Promise1((resolve) => {
    resolve();
});
promise1
    .then(() => console.log(1))
    .then(() => console.log(2))
    .then(() => console.log(3));

const promise2 = new Promise2((resolve) => {
    resolve();
});
promise2
    .then(() => console.log(4))
    .then(() => console.log(5))
    .then(() => console.log(6));

天真地以为 Promise.then 会将异步操作连续且按顺序一系列的推进微任务队列,然而事情的结果总没有想象中的简单,正确的输出顺序是: 1 4 2 5 3 6

  • 先引出两条结论:
  1. Promise 多个 then() 链式调用,并不是连续的创建了多个微任务并推入微任务队列。then() 的返回值是必然一个新的 Promise 实例,而后续的 then() 是上一步 then() 返回的 Promise 的回调。
  1. then 负责注册回调函数,当 Promise 状态发生了变化时,才会触发把对应的回调函数推入微任务队列。也就是说 then() 方法会先注册回调函数,直到前面的 Promise 变成了 resolved/rejected 状态,才会将回调推入微任务队列中,若前面的 Promise 依然是 pending,则将回调存储在 Promise 内部,一直等到状态被改变。
  • 根据以上结论下面来好好地分析一下过程:

(1)首先,执行 promise1 构造器内部的同步代码,执行到 resolve(),将 promise1 的状态改变为 Promise { <resolved> : undefined } ,然后第一个 then 中传入的回调函数 console.log('1') 被作为一个微任务被推入微任务队列,而在此之后的第二个 then() 传入的回调函数需要等到第一个 then() 新创建的 Promise 实例 resolved 了以后(即等到 console.log('1') 输出以后)才会往下执行。

任务
主线程 promise2 同步代码
微任务 console.log('1')

(2)接着,执行 promise2 同步代码,同样地执行到了 resolve(),将 promise2 的状态改变为 Promise { <resolved> : undefined },然后 then 中传入的回调函数 console.log('4') 作为一个微任务被推入微任务队列。

任务
主线程
微任务 console.log('1')console.log('4')

(3)主线程已经没有任务了,所以将会去检查微任务队列,发现有微任务,将会按顺序一一执行。先执行console.log('1'),此时 promise1 的第一个 then() 状态变成了 resovled,会触发把 promise1 的第二个 then 中传入的回调函数 console.log('2') 被作为一个微任务被推入微任务队列。 此时控制台会先输出: 1。

任务
主线程
微任务 console.log('4')console.log('2')

同样地,等到 1 被输出后,会执行下一个微任务 console.log('4') ,而此时 promise2 的第一个 then() 也会状态变成了 resovled,会触发把 promise2 的第二个 then 中传入的回调函数 console.log('5') 被作为一个微任务被推入微任务队列。此时控制台输出: 1 4。

任务
主线程
微任务 console.log('2')console.log('5')

(4)接下来的程序像第 3 步所描述那样子,往复执行,直到所有任务被执行完毕。最后,控制台输出:1 4 2 5 3 6

# Promise.resolve

静态方法 Promise.resolve 可以认为是 new Promise() 的快捷使用方式:

new Promise(function(resolve) {
    resolve(42);
}).then((value) => {
    console.log(value);
});

// 等价于
Promise.solve(42).then((value) => {
    console.log(value);
});

Promise.resolve 用于返回一个已给参数解析后的 Promise 对象。其中,参数有四种情况:

  • 参数为一个原始值,或者为一个不具有 then 方法的对象
    如果参数是一个原始值,或者是一个不具有 then 方法的对象时,则 Promise.resolve 会返回一个新的状态为 resolved 的 Promise 对象,并将参数传给回调函数。

  • 没有参数
    当 Promise.resolve 方法调用不带参数时,会直接返回一个新的状态为 resolved 的 Promise 对象。

  • 参数为一个 thenable 对象
    thenable 对象是指具有 then 方法的对象,比如:

    let thenable = {
        then: (resolve, reject) => {
            resolve("i am from thenable");
        },
    };
    

    Promise.resolve 会将此 thenable 对象转为 Promise 对象,并且之后会立即执行 thenable 对象里面的 then 方法,比如:

    let thenable = {
        then: (resolve, reject) => {
            resolve("i am from thenable");
        },
    };
    
    Promise.resolve(thenable).then((result) => {
        // i am from thenable
        console.log(result);
    });
    

    then 方法执行完以后,Promise 对象状态也将转为其 then 方法里面的对应状态。

    同时,也引入来自令人费解的 async/await 执行顺序 (opens new window)中,对 TC39 规范中 Promise Resolve Functions 的描述总结:

    1. 如果有一个对象 obj,且 obj.then 是一个 function ,那么 obj 可以被称为thenable 对象;

    2. 当在 Promise 中 resolve 一个 thenable 对象时(new Promise(resolve => resolve(thenable)) ),需要一个过程去将 thenable 对象转成 Promise,并且会立即调用 thenable 对象里面 then 的方法。这个过程需要作为一个任务(PromiseResolveThenableJob),推进当前的微任务队列,以保证对 then 方法的解析发生在其他上下文代码解析之后。

    可能有点绕,来看看表达上述意思的例子:

    let thenable = {
        then(resolve, reject) {
            console.log("in thenable");
            resolve(100);
        },
    };
    
    new Promise((resolve) => {
        console.log("in p0");
        resolve(thenable);
    }).then(() => {
        console.log("thenable ok");
    });
    
    new Promise((resolve) => {
        console.log("in p1");
        resolve();
    })
        .then(() => {
            console.log("1");
        })
        .then(() => {
            console.log("2");
        })
        .then(() => {
            console.log("3");
        })
        .then(() => {
            console.log("4");
        });
    

    执行结果: in p0 -> in p1 -> in thenable -> 1 -> thenable ok -> 2 -> 3 -> 4

    解析:

    此处,将第一个 Promise 简称为 promise1,将第二个 Promise 简称为 promise2。

    (1)首先,执行 promise1 同步代码,输出in p0,当代码执行到 resolve(thenable), 此处的 resolve 会将 thenable 对象转化为 Promise 的这一过程(即 PromiseResolveThenableJob )作为一个微任务推进微任务队列等待执行,而 promise1 的外部一个 then 需要等待这个微任务被 resolved 后才会被推入微任务队列

    任务
    主线程 promise2 同步代码
    微任务 thenable

    (2)然后继续往下执行 promise2 同步代码,输出in p1,当代码执行到 resolve(),会将 promise2 的状态变成 Promise {<resolved>: undefined},然后 promise2.then 的第一个回调函数 console.log('1') 会被作为一个微任务被推入微任务队列。此时控制台输出:in p0 -> in p1

    任务
    主线程
    微任务 thenableconsole.log('1')

    (3)主线程这时没有任务了,接着会检索微任务队列中的任务,并会按顺序执行。这里会先对 thenable 对象进行处理,这时候会输出 in thenable ,当代码执行到 resolve(100) ,这个微任务状态已被改变为 Promise {<resolved>: 100},promise1 外部的第一个 then 回调将会被推入微任务队列。此时控制台输出:in p0 -> in p1 -> in thenable

    任务
    主线程
    微任务 console.log('1')console.log('thenable ok')

    接着,执行 promise2 的第一个 then 回调 console.log('1')(状态变成了 resolved),紧接着 promise2 的第二个 then 回调 console.log('2') 会被推入微任务队列。此时控制台输出:in p0 -> in p1 -> in thenable -> 1

    任务
    主线程
    微任务 console.log('thenable ok')console.log('2')

    (4)接着,执行 promise1 的最后一个回调函数,输出 thenable ok 。promise1 的整个过程已经执行完,接着执行 promise2 的第二个 then 回调,输出 2。剩下的,就接着一步步执行,直至控制台输出:in p0 -> in p1 -> in thenable -> 1 -> thenable ok -> 2 -> 3 -> 4

    上述的resolve(thenable)可转化为:

    new Promise((resolve) => {
        console.log("in p0");
        resolve(thenable);
    }).then(() => {
        console.log("thenable ok");
    });
    
    // 等价于
    new Promise((resolve) => {
        console.log("in p0");
    
        Promise.resolve().then(() => {
            thenable.then(resolve);
        });
    });
    
  • 参数为一个 Promise 对象
    当参数是一个 Promise 对象时,那么 Promise.resolve 会将这个 Promise 对象不作任何修改地返回。

    注意: 此时的Promise.resolve(v) 不等于 new Promise(r => r(v)),因为如果 v 是一个 Promise 对象,前者会直接返回 v,而后者需要经过一系列的处理(主要是 PromiseResolveThenableJob)。

    由于 Promise 实例是一个对象,其原型上有 then 方法,所以这也是一个 thenable 对象。
    同样的,浏览器会创建一个 PromiseResolveThenableJob 去处理这个 Promise 实例,这是一个微任务。在 PromiseResolveThenableJob 执行中,执行了 Promise.prototype.then,而这时 Promise 如果已经是 resolved 状态 ,then 的执行会再一次创建了一个微任务。
    最终结果就是:额外创建了两个 Job,表现上就是后续代码被推迟了 2 个时序。

# async

关于 async 函数,是 ES6 中 Generator 函数的语法糖(和 Generator 的区别什么的就不展开了...)。语义上也表明了带 async 关键字的函数中有异步操作,而这类函数的返回值必定一个是 promise 对象,可以使用 then 方法进行后续的操作。

如果 async 关键字函数返回的不是 promise,会自动用 Promise.resolve() 包装(除非被抛出错误,会使用 Promise.reject())。如果 async 关键字函数显式地返回 promise,那就以返回的 promise 为准。如果 async 关键字函数没有返回值,就会返回 Promise.resolve(undefined)。

例子 1

async function asnyc1() {
    return "i am from async1 function";
}
async1.then((res) => {
    console.log(res);
});
console.log("start");

输出的结果先后顺序为: start -> i am from async1 function 虽然是上面 asnyc1()先执行了,但是因为被定义成了异步函数,所以不会影响后续函数的执行。

那关于 await 的呢,一般 await 关键字只能在 async 关键字函数的内部出现。async 函数必须等到内部所有的 await 命令执行完,才会发生状态改变。 那 await 在 async 函数中在等什么呢?

# await 在等什么?

在没有使用 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且绝不会阻塞后面的语句,Promise 的特点之一是无等待,所以这和普通返回 Promise 对象的函数使用起来没太大差别。

一旦使用 await,情况就有点不太一样了。一般可认为 await 是在等待 async 函数完成。(因为 async 函数返回的是一个 promise 对象,所以 await 可以用于等待一个 async 函数的返回值。)但按语法 (opens new window)来说,await 是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的结果。

await 是一个让出线程的标志,阻塞 async 函数内部出现在 await 后面的代码。比如下面的例子:

async function async1() {
    console.log("async1 start");
    await async2();
    console.log("async1 end");
}
async function async2() {
    console.log("async2");
}
async1();
console.log("script start");

在例子中,遇到 await async2()时,会先执行 await 后面的函数 async2(),将 await 后面的代码加入到微任务队列中,后将会跳出整个 async1 函数,接着执行外部代码,最后等到本轮事件循环执行完了之后,再接着跳回 async1 函数中等待 await 后面表达式的返回值

值得注意的是,此时等待的返回值又分为两种情况:

  1. 如果返回值为非 Promise 对象则把这个非 Promise 东西作为 await 表达式的结果,然后继续执行 async 函数内部后面的代码(await 后面的代码可以简单理解为是.then 的回调事件);
  1. 如果返回值为 Promise 对象则将返回的 Promise 放入 Promise 队列,等待这个对象被 resolve 后,把 resolve 的参数作为 await 表达式结果。(因此有这样子的说法:async 函数的调用是不会造成阻塞,因为一旦执遇到 await 就先返回,它内部所有的“阻塞”都被封装在一个 Promise 对象中异步执行,等到这个 Promise 异步操作完成以后,再接着执行函数体内后面的代码。如果这个 Promise 没被 resolve,await 后续的代码将不会执行。)

即上述例子代码可转化为:

async function async1() {
    console.log("async1 start");
    const p = async2();
    return Promise.resolve(p).then(() => {
        console.log("async1 end");
    });
}

async function async2() {
    console.log("async2");
}
async1();
console.log("script start");

所以,上面的例子输出顺序应该是:async1 start -> async2 -> script start -> async1 end

留下的疑问:
async/await 和 promise 的区别在哪?
async/await 的优势在哪?
什么时候应该选择用 async/await?

# 梳理总体的例子

来来来,再来一个例子梳理一下以上全文总体:

async function async1() {
    console.log("async1 start");
    await async2();
    //更改如下:
    setTimeout(function() {
        console.log("setTimeout1");
    }, 0);
}
async function async2() {
    //更改如下:
    setTimeout(function() {
        console.log("setTimeout2");
    }, 0);
}
console.log("script start");

setTimeout(function() {
    console.log("setTimeout3");
}, 0);
async1();

new Promise(function(resolve) {
    console.log("promise1");
    resolve();
}).then(function() {
    console.log("promise2");
});
console.log("script end");

这里依次输出的结果:

script start
async1 start
promise1
script end
promise2
setTimeout3
setTimeout2
setTimeout1

似懂未懂?再来一题:

function testSometing() {
    console.log("in testSomething");
    return "return testSomething";
}

async function testAsync() {
    console.log("in testAsync");
    return Promise.resolve("hello async");
}

async function test() {
    console.log("test start...");

    // 关键点1
    const testFn1 = await testSometing();
    console.log(testFn1);

    // 关键点2
    const testFn2 = await testAsync();

    console.log(testFn2);
    console.log(testFn1, testFn2);
}

test();

const promiseFn = new Promise((resolve) => {
    console.log("promise start");
    resolve("promise resolve");
});
promiseFn.then((val) => console.log(val));

console.log("test end...");

这一题搬来小椅子,逐个来好好分析分析:

(1)首先执行test(),会输出:console.log("test start..."),然后执行到const testFn1 = await testSometing();,此时会先执行 testSomething(),输出console.log("in testSomething"),此时 await 将会让出线程,就跳出test()函数先执行函数体外的代码(可以理解为 await 下面的代码都被当成微任务推进微任务队列)。

(2)接着执行到promiseFn,先输出console.log("promise start"),往下执行到resolve("promise resolve"),promiseFn 的 then 回调将会被推入微任务队列。同时,继续往下执行,输出console.log("test end...")

此时,主线程的任务被执行完,输出结果:

test start...
in testSomething
promise start
test end...

(3)然后检查微任务队列,即跳回test()函数体内继续往下执行。等待到 testSomething() 返回的是非 Promise 对象,继续往下执行,即先输出console.log(testFn1),即 return testSomething

(4)然后执行到const testFn2 = await testAsync(),输出console.log("in testAsync")后,又会再次让出线程,执行下一个微任务(即 promiseFn 的 then 回调promiseFn.then(val => console.log(val))),输出"promise resolve"

至此,此时的输出结果

test start...
in testSomething
promise start
test end...
return testSomething
in testAsync
promise resolve

如果对让出线程这个说法还是有点难理解,可以通过对关键点 1 和关键点 2 的转化来理解:

async function test() {
    console.log("test start...");

    // 关键点1
    // const testFn1 = await testSometing();
    // console.log(testFn1);

    // 关键点2
    // const testFn2 = await testAsync();
    // console.log(testFn2);
    // console.log(testFn1, testFn2);

    const p1 = testSometing();

    // 将p1的回调推入微任务队列
    Promise.resolve(p1).then((res1) => {
        console.log(res1);
        const p2 = testAsync();

        // 将p2的回调推入微任务队列
        Promise.resolve(p2).then((res2) => {
            console.log(res2);
            console.log(res1, res2);
        });
    });
}

(4)最后,再回到test()函数体内,等待testAsync()返回的 Promise 对象被 resolve 后,往下执行console.log(testFn2)console.log(testFn1, testFn2)

最终输出结果:

test start...
in testSomething
promise start
test end...
return testSomething
in testAsync
promise resolve
hello async
return testSomething, hello async

# 参考文章

序号 文章
1 令人费解的 async/await 执行顺序 (opens new window)
2 理解 JavaScript 的 async/await (opens new window)
3 Promise 链式调用顺序引发的思考 (opens new window)
4 async await 和 promise 微任务执行顺序问题 (opens new window)